package ac.il.technion.c236369.hw4.configuration;

import java.util.ArrayList;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

/**
 * @author Artem Barger, Maxim Aizenshtein
 * 
 * Server configuration storage class. Keep all information
 * which available inside server configuration file.
 * ServerConfiguration class implements "Singleton" design pattern. 
 *
 */
public class ServerConfig {

	private static final String CONFIG_FILE = "config.xml";
	private final static ServerConfig instance = new ServerConfig( );
	private final Properties properties = new Properties( );
	private final Logger log = Logger.getLogger( ServerConfig.class);
	
	public final static String DEFAULT_MIME_TYPE = "<def>";
	
	/**
	 * @author Artem Barger, Maxim Aizenshtein
	 * Helper inner file, which is keeps and represents
	 * all relevant server configurations options.
	 * 
	 */
	public static class Properties {
		private Logger log = Logger.getLogger( Properties.class);
		private String base;
		private String port;
		private Integer socketReaders;
		private Integer requestHandlers;
		private Integer socketQueue;
		private Integer requestQueue;

		private Map< String, List<String>> typeHandlers 	= new HashMap< String, List<String>>( );
		private Map< String, String> mimeTypes 		= new HashMap< String, String>( );
		private List<String> welcomeFiles	= new ArrayList<String>( );

		/**
		 * Add new type handler to existing set of handlers.
		 * 
		 * @param ext - file extension
		 * @param className - class name to be associated with file extension
		 */
		public void addTypeHandler( String ext, String className) {
			if ( !typeHandlers.containsKey(className))
				typeHandlers.put( className, new ArrayList<String>( ));
			typeHandlers.get(className).add(ext);
			if ( log.isDebugEnabled()) {
				Formatter formatter = new Formatter();
				log.debug( formatter.format( "Adding type handler: <%s> for files extension: <%s>.", className , ext));
			}
		}

		/**
		 * Add new mime type to existing set of available mime types.
		 * 
		 * @param ext - file extension
		 * @param mimeType - mime type to be posted inside response content type
		 * for given file extension.
		 */
		public void addMimeType( String ext, String mimeType) {
			mimeTypes.put( ext, mimeType);
			if ( log.isDebugEnabled()) {
				Formatter formatter = new Formatter();
				log.debug( formatter.format( "Adding mime type: <%s> for files extension: <%s>.", mimeType , ext));
			}
		}
		
		/**
		 * Removes type handler form collection of available handlers on the server.
		 * 
		 * @param className - type handler class name. 
		 */
		public void removeTypeHandler( String className) {
				typeHandlers.remove(className);
				if ( log.isDebugEnabled()) {
					Formatter formatter = new Formatter( );
					log.debug( formatter.format( "Deleting type handler: <%s>", className));
				}
		}

		/**
		 * Remove mime type.
		 * 
		 * @param ext - file extension
		 */
		public void removeMimeType( String ext) {
			mimeTypes.remove( ext);
			if ( log.isDebugEnabled( )) {
				Formatter formatter = new Formatter( );
				log.debug( formatter.format( "Deleting mime type for file extension: <>", ext));
			}
		}
		
		/**
		 * @return location of server root document directory.
		 */
		public String getBase() {
			return base;
		}
		
		/**
		 * Set folder name for default root server documents directory
		 * 
		 * @param base - folder name on disk storage
		 */
		public void setBase(String base) {
			if ( log.isDebugEnabled( )) {
				Formatter formatter = new Formatter( );
				log.debug( formatter.format( "Server documents root directory is: <>", base));
			}
			this.base = base;
		}
		
		/**
		 * @return port server listening on
		 */
		public Integer getPort() {
			return Integer.valueOf( port);
		}
		
		/**
		 * Set port number for server to listen on.
		 * 
		 * @param port
		 */
		public void setPort(String port) {
			if ( log.isDebugEnabled( )) {
				Formatter formatter = new Formatter( );
				log.debug( formatter.format( "Server listening on port: <>", port));
			}
			this.port = port;
		}
		
		/**
		 * @return size of permitted sockets readers threads count
		 */
		public Integer getSocketReaders() {
			return socketReaders;
		}
		
		/**
		 * Set sockets reader count on server
		 * 
		 * @param socketReaders 
		 */
		public void setSocketReaders(Integer socketReaders) {
			if ( log.isDebugEnabled( )) {
				Formatter formatter = new Formatter( );
				log.debug( formatter.format( "Server sockets readers threads are limited to count: <>", socketReaders));
			}
			this.socketReaders = socketReaders;
		}
		
		/**
		 * @return  - size of server sockets queue size 
		 */
		public Integer getSocketQueue() {
			return socketQueue;
		}
		
		/**
		 * Set server sockets queue size
		 * 
		 * @param socketQueue
		 */
		public void setSocketQueue(Integer socketQueue) {
			if ( log.isDebugEnabled( )) {
				Formatter formatter = new Formatter( );
				log.debug( formatter.format( "Server socket queue size is limited to size: <>", socketQueue));
			}
			this.socketQueue = socketQueue;
		}
		
		/**
		 * @return server requests queue size
		 */
		public Integer getRequestQueue() {
			return requestQueue;
		}
		
		/**
		 * Set server's request queue size. 
		 * 
		 * @param requestQueue
		 */
		public void setRequestQueue(Integer requestQueue) {
			if ( log.isDebugEnabled( )) {
				Formatter formatter = new Formatter( );
				log.debug( formatter.format( "Server requests queue size is limited to size: <>", requestQueue));
			}
			this.requestQueue = requestQueue;
		}
		
		/**
		 * @return list of default welcome file in for document folders.
		 */
		public List<String> getWelcomeFiles() {
			return welcomeFiles;
		}

		/**
		 * @return number of requests handlers threads available on server.
		 */
		public Integer getRequestHandlers() {
			return requestHandlers;
		}

		/**
		 * Set count for request handlers threads on server.
		 * 
		 * @param requestHandlers
		 */
		public void setRequestHandlers(Integer requestHandlers) {
			if ( log.isDebugEnabled( )) {
				Formatter formatter = new Formatter( );
				log.debug( formatter.format( "Server requests handlers are limited to count: <>", socketQueue));
			}
			this.requestHandlers = requestHandlers;
		}

		/**
		 * @return map of type handlers, where for each type handlers 
		 * returns list of files extensions
		 */
		public Map<String, List<String>> getTypeHandlers() {
			return typeHandlers;
		}

		/**
		 * @return map of mime types available on server
		 */
		public Map<String, String> getMimeTypes() {
			return mimeTypes;
		}

		/**
		 * Add default welcome file template
		 * 
		 * @param nodeValue
		 */
		public void addWelcomeFile(String nodeValue) {
			if ( log.isDebugEnabled( )) {
				Formatter formatter = new Formatter( );
				log.debug( formatter.format( "Adding default welcome file: ", nodeValue));
			}
			welcomeFiles.add( nodeValue);
		}
		
		/**
		 * Remove default welcome file on server.
		 * 
		 * @param nodeValue
		 */
		public void removeWelcomeFile(String nodeValue) {
			welcomeFiles.remove( nodeValue);
		}
	}
	
	private ServerConfig( ) {
	    try {
			DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
			domFactory.setNamespaceAware(true); // never forget this!
			DocumentBuilder builder = domFactory.newDocumentBuilder();
			if ( log.isDebugEnabled( )) {
				log.debug( "Start parsing " + CONFIG_FILE + " server configuration file.");
			}

			Document doc = builder.parse(CONFIG_FILE);

			XPathFactory factory = XPathFactory.newInstance();
			XPath xpath = factory.newXPath( );
			readServerBaseConfiguration( doc, xpath);
			readThreadsConfiguration(doc, xpath);
			readQueuesConfiguration(doc, xpath);
			readWelcomeFilesConfiguration(doc, xpath);
			readMimeTypesConfiguration(doc, xpath);
			readTypeHandlersConfiguration(doc, xpath);
			if ( log.isDebugEnabled( )) {
				log.debug( "Finished parsing of" + CONFIG_FILE + " server configuration file.");
			}
		} catch (Exception e) {
			log.error( e);
		}
	}

	private void readTypeHandlersConfiguration(Document doc, XPath xpath)
			throws XPathExpressionException {
		NodeList results = (NodeList)xpath.evaluate( "//server-config//type-handler/extension/text()", doc, XPathConstants.NODESET);
		for ( int i = 0; i < results.getLength(); ++i) {
			String ext = results.item(i).getNodeValue( );
			String className = xpath.evaluate( "//server-config//type-handler[extension='" + ext +"']/@class", doc);
			properties.addTypeHandler(ext, className);
		}
	}

	private void readMimeTypesConfiguration(Document doc, XPath xpath)
			throws XPathExpressionException {
		NodeList results = (NodeList)xpath.evaluate( "//server-config//mime-mapping/extension/text()", doc, XPathConstants.NODESET);
		for ( int i = 0; i < results.getLength(); ++i) {
			String ext = results.item(i).getNodeValue( );
			String mimeType = xpath.evaluate( "//server-config//mime-mapping[extension='" + ext +"']/mime-type/text()", doc);
			properties.addMimeType(ext, mimeType);
		}
		properties.addMimeType( DEFAULT_MIME_TYPE, "text/html; charset=UTF-8");
	}

	private void readWelcomeFilesConfiguration(Document doc, XPath xpath)
			throws XPathExpressionException {
		XPathExpression expr = xpath.compile("/server-config/welcome-file/text( )");
		NodeList result = (NodeList)expr.evaluate( doc, XPathConstants.NODESET);
		for ( int i = 0; i < result.getLength( ); ++i) {
			properties.addWelcomeFile( result.item(i).getNodeValue( ));
		}
	}

	private void readQueuesConfiguration(Document doc, XPath xpath)
			throws XPathExpressionException {
		properties.setSocketQueue( Integer.valueOf( xpath.evaluate( "//queues/socket-queue/size/text( )", doc)));
		properties.setRequestQueue( Integer.valueOf( xpath.evaluate( "//queues/socket-queue/size/text( )", doc)));
	}

	private void readThreadsConfiguration(Document doc, XPath xpath)
			throws XPathExpressionException {
		properties.setSocketReaders( Integer.valueOf( xpath.evaluate( "//threads/socket-readers/multi/text( )", doc)));
		properties.setRequestHandlers( Integer.valueOf( xpath.evaluate( "//threads/request-handlers/multi/text( )", doc)));
	}

	private void readServerBaseConfiguration(Document doc, XPath xpath)
			throws XPathExpressionException {
		properties.setBase( xpath.evaluate( "/server-config/@base", doc));
		properties.setPort( xpath.evaluate( "/server-config/@port", doc));
	}
	
	public Properties getProperties() {
		return properties;
	}

	public static ServerConfig getInstance( ) {
		return instance;
	}
}
